<?php
// 我们需要使用 SESSION 来保存 uac 的 url，避免每次都远程获取
if (empty($_SESSION)) @session_start();

require_once dirname(__FILE__).'/K12SSO_Factory.class.php';
require_once dirname(__FILE__).'/vendor/xmlrpc/xmlrpc.inc'; //包含 XMLRPC 库文件

/**
 * UAC 单点登录整合类。
 *
 * @package K12SSO
 * @author Nio Xiao
 */
class K12SSO_UAC implements K12SSO_Interface
{
    /**
     * 用于保存 UAC url 的 SESSION 名字。
     * @var string
     */
    private static $url_sessname = __CLASS__;
    /**
     * 单点登录需要的 UAC XMLRPC 参数设置。
     * @var array
     */
    private $options = array();
    /**
     * 最后的错误信息。
     * @var string
     */
    private $error = '';
    
    //================================= K12SSO_Interface Start =================================

    /**
     * 构建函数。
     *
     * @param array $options 初始化参数设置数组，需要设置 UAC XMLRPC 参数：
     *                          array(
     *                              'base_url'     => 'UAC 基础 URL',
     *                              'xmlrpc_host'  => 'XMLRPC 服务主机地址',
     *                              'xmlrpc_port'  => 'XMLRPC 服务端口',
     *                              'xmlrpc_path'  => 'XMLRPC 服务路径',
     *                              'xmlrpc_user'  => 'XMLRPC 服务访问用户名',
     *                              'xmlrpc_pass'  => 'XMLRPC 服务访问密码',
     *                              'xmlrpc_debug' => '是否打开 XMLRPC 客户端调试状态',
     *                          )
     * @return K12SSO_Interface
     */
    public function __construct($options = array())
    {
        if (isset($options['base_url'])) $options['base_url'] = rtrim($options['base_url'], '/').'/';
        $this->options = $options;
        if (empty($this->options['encoding'])) $this->options['encoding'] = 'UTF-8';
    }
    
    /**
     * 执行登录操作。
     *
     * @param string $url 登录之后返回的 URL 。
     * @param string $var 单点登录的 GET 变量名。
     * @param string $from 登录来源，如：主网 CMS 使用 xhedu，节点平台使用 node。
     * @return string 登录成功之后，返回已登录的用户名。
     */
    public function doLogin($url = null, $var = 'sessid', $from = '')
    {
        $sessid = isset($_GET[$var]) ? $_GET[$var] : '';
        // dd($sessid);
        if ($sessid) { //UAC 登录完成，跳回到当前页面并附加 sessid 参数，通过此参数来获取当前登录的用户名
            $user_id = $this->getLoggedInUserId($sessid);
            if ($user_id === false) die($this->getError());
            if ($user_id) return $user_id;
        }
        //没有 sessid 参数，或从 UAC 中获取用户名失败，跳转到 UAC 登录页面
        if (!$url) $url = K12SSO_Factory::getFullRequestUri();
        $url = self::clearUrl($url);
        $login_url = $this->getPlatformUrl() . 'app/login.php?url=' . urlencode($url);
        if ($from) $login_url .= '&from='.urlencode($from);
        header('Location: ' . $login_url);
        exit;
    }
    
    /**
     * 执行登出操作。
     *
     * @param string $url 登出之后返回的 URL 。
     */
    public function doLogout($url = null)
    {
        if (!$url) $url = K12SSO_Factory::getFullRequestUri();
        $url = self::clearUrl($url);
        header('Location: ' . $this->getPlatformUrl() . 'app/logout_do.php?url=' . urlencode($url));
        exit;
    }

    /**
     * 获取最后的错误信息。
     *
     * @return string
     */
    public function getError()
    {
        return $this->error;
    }
    
    //================================= K12SSO_Interface End =================================
    
    /**
     * 通过远程调用，获取 UAC 的平台 URL，此 URL 对于内外网的客户端可能会有所不同。
     *
     * @return string
     */
    public function getPlatformUrl()
    {
        if (empty($this->options['base_url'])) {
            if (empty($_SESSION[self::$sessname])) {
                $_SESSION[self::$url_sessname] = $this->callRpcMethod('base.getPlatformUrl', array(K12SSO_Factory::getRemoteAddr())).'/platform/';
            }
            $url = $_SESSION[self::$url_sessname];
        } else {
            $url = rtrim($this->options['base_url'], '/').'/';
        }
        return $url;
    }
    
    /**
     * 通过远程调用，获取当前登录的用户名。
     *
     * @param string $sessid UAC 传给应用的用户会话参数。
     * @return string|boolean 成功时返回用户名，失败则返回 false，可通过 getError() 方法获取到错误信息。
     */
    public function getLoggedInUserId($sessid)
    {
        return $this->callRpcMethod('user.isUserLogin', array($sessid));
    }
    
    /**
     * 通过远程调用，获取用户信息。
     *
     * @param string $user_id 用户名。
     * @return array|boolean 成功时返回用户信息数组，失败则返回 false，可通过 getError() 方法获取到错误信息。
     */
    public function getUserInfo($user_id)
    {
        $good_user_id = str_replace(array("'", '"'), '', $user_id);
        $retval = $this->getUsers("user_id = '".$good_user_id."'", '', 1, 0);
        if ($retval === false) return false;
        if ($retval && is_array($retval)) {
            return $retval[0];
        } else {
            $this->error = '未找到用户 '.$user_id.'。';
            return false;
        }
    }

    /**
     * 通过远程调用，获取多个用户信息。
     *
     * @param string $user_id 用户名。
     * @return array|boolean 成功时返回用户信息数组，失败则返回 false，可通过 getError() 方法获取到错误信息。
     */
    public function getUsersByUserIds($user_ids)
    {
        foreach ($user_ids as $user_id) {
            if (preg_match('/[^a-z0-9_]/i', $user_id)) {
                $this->error = '用户名 '.$user_id.' 不合法。';
                return false;
            }
        }
        if ($user_ids) {
            $retval = $this->getUsers("user_id IN ('".implode("','", $user_ids)."')");
            if ($retval === false) return false;
            if ($retval && is_array($retval)) {
                if (count($retval) == count($user_ids)) {
                    return $retval;
                } else {
                    foreach ($retval as $u) {
                        $pos = array_search($u['user_id'], $user_ids);
                        if ($pos !== false) unset($user_ids[$pos]);
                    }
                    $this->error = '未找到用户名：'.implode(', ', $user_ids).'。';
                    return false;
                }
            } else {
                $this->error = '未找到符合相关用户。';
                return false;
            }
        } else {
            return array();
        }
    }

    /**
     * 查询用户信息。
     *
     * @param string $where WHERE 查询子句。
     * @param string $order ORDER BY 查询子句。
     * @param integer $numrows 返回最大记录数。
     * @param integer $offset 其实记录位置。
     * @return array|boolean 成功时返回用户信息数组，失败则返回 false，可通过 getError() 方法获取到错误信息。
     */
    public function getUsers($where, $order = '', $numrows = 50, $offset = 0)
    {
        $params = array($where, $order, $numrows, $offset);
        $retval = $this->callRpcMethod('user.getUserInfo', $params);
        if ($retval === false) return false;
        return $retval;
    }

    /**
     * 检测用户名、密码是否正确。
     *
     * @param string $user_id 用户名。
     * @param string $password 密码。
     * @return boolean 正确返回 true，错误返回 false。
     */
    public function checkUserPassword($user_id, $password)
    {
        $uri  = ($this->options['xmlrpc_port'] == 443) ? 'https' : 'http';
        $uri .= '://'.$this->options['xmlrpc_host'];
        if ($this->options['xmlrpc_port'] != 80 && $this->options['xmlrpc_port'] != 443) {
            $uri .= ':'.$this->options['xmlrpc_port'];
        }
        $uri .= preg_replace('#/[^/]*$#i', '/password.php', $this->options['xmlrpc_path']);
        $data = array('user_id'=>$user_id, 'password'=>$password);
        try {
            $retval = intval($this->httpRequest($uri, $data, 'GET'));
            return ($retval == 1);
        } catch (Exception $e) {
            $this->error = $e->getMessage();
            return false;
        }
    }

    /**
     * 获取 UAC 代理登录页面的完整 URL。
     *
     * @param string $user_id 用户名。
     * @param string $service 代理登录成功之后的跳转 URL。
     * @return string
     */
    public function getProxyLoginUrl($user_id, $service)
    {
        $uri  = ($this->options['xmlrpc_port'] == 443) ? 'https' : 'http';
        $uri .= '://'.$this->options['xmlrpc_host'];
        if ($this->options['xmlrpc_port'] != 80 && $this->options['xmlrpc_port'] != 443) {
            $uri .= ':'.$this->options['xmlrpc_port'];
        }
        $uri .= preg_replace('#/[^/]*$#i', '/token.php', $this->options['xmlrpc_path']);
        $data = array('user_id'=>$user_id);
        try {
            $token = trim($this->httpRequest($uri, $data, 'GET'));
            if (!$token) throw new Exception('得到的用户认证令牌为空。');
            return $this->getPlatformUrl().'app/connect.php?u='.urlencode($user_id).'&t='.urlencode($token).'&s='.urlencode($service);
        } catch (Exception $e) {
            $this->error = $e->getMessage();
            return false;
        }
    }
    
    /**
     * 调用 UAC 远程方法调用。
     *
     * @param string $method 远程方法名。
     * @param array $params 参数数组（PHP 类型）。
     * @return mixed|boolean 成功返回远程调用的返回值（PHP 类型），失败则返回 false，
     *                       可通过 getError() 方法获取到错误信息。
     */
    public function callRpcMethod($method, $params = array())
    {
        $this->error = '';
        $c = new xmlrpc_client($this->options['xmlrpc_path'], $this->options['xmlrpc_host'], $this->options['xmlrpc_port'], ($this->options['xmlrpc_port'] == 443 ? 'https' : 'http11'));
		if ($this->options['xmlrpc_port'] == 443) {
		    $c->setSSLVerifyPeer(false);
		    $c->setSSLVerifyHost(2);
		}
		$c->request_charset_encoding = $this->options['encoding'];
        $c->setDebug($this->options['xmlrpc_debug']);
        $c->setCredentials($this->options['xmlrpc_user'], $this->options['xmlrpc_pass']); //Authorization
        foreach ($params as $k => $param) $params[$k] = php_xmlrpc_encode($param);
        $m = new xmlrpcmsg($method, $params);
        $r = $c->send($m);
        $errmsg_prefix = '访问基础平台（' . $this->options['xmlrpc_path'] . '）的 XMLRPC 方法 ' . $method . ' 失败！';
        if (!$r) {
            $errmsg = $errmsg_prefix.'原因：消息发送失败！';
        } elseif ($r->faultCode()) {  //ERROR!
            $errmsg = $errmsg_prefix.'错误：' . $r->faultCode() . ' - ' . $r->faultString();
        }   //end if
        if (empty($errmsg)) {
            return php_xmlrpc_decode($r->value());
        } else {
            $this->error = $errmsg;
            return false;
        }
    }
    
    /**
     * 获取 UAC 顶部导航条。
     *
     * @param string $login_redirect_url 登录成功之后的跳转地址。
     * @param string $logout_redirect_url 注销成功之后的跳转地址。
     * @param string $session_change 登录状态变化回调的 javascript 代码。
     * @param string $topnav_id 用于放置顶部导航栏的 DOM ID。
     * @return string
     */
    public function getTopNav($login_redirect_url, $logout_redirect_url, $session_change = '', $topnav_id = 'topnav')
    {
        return '
            <div id="'.$topnav_id.'"></div>
            <script type="text/javascript">
                var _global_is_login = false;
                var _kucfg = _kucfg || {};
                _kucfg.dom_id  = "'.$topnav_id.'"; //放置用户信息头内容的 DOM ID
                _kucfg.uac_url = "'.$this->getPlatformUrl().'u.php";
                _kucfg.session_change = function(is_login){
                    '.$session_change.'
                };
                _kucfg.login_redirect_url = "'.$login_redirect_url.'";
                _kucfg.logout_redirect_url = "'.$logout_redirect_url.'";
                (function() {
                    var _ku = document.createElement("script"); _ku.type = "text/javascript"; _ku.async = true;
                    _ku.src = _kucfg.uac_url;
                    var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(_ku, s);
                })();
            </script>
        ';
    }
    
    /**
     * 获取 UAC 用户头像地址。
     *
     * @param string $user_id 用户名。
     * @param integer $size 头像尺寸大小，目前支持 32、48、64、100、150 和 200。
     * @return string
     */
    public function getAvatarUrl($user_id, $size = 100)
    {
        return $this->getPlatformUrl() . 'avatar/' . md5(strtolower($user_id)) . '/' . $size . '?' . rand(100000, 999999);
    }

    /**
     * 获取 UAC 修改用户个人信息的地址。
     *
     * @return string
     */
    public function getEditProfileUrl()
    {
        return $this->getPlatformUrl() . '?t=profile_edit';
    }

    /**
     * 获取 UAC 修改用户头像的地址。
     *
     * @return string
     */
    public function getEditAvatarUrl()
    {
        return $this->getPlatformUrl() . '?t=profile_avatar';
    }
    
    /**
     * 获取 UAC 用户注册页面的地址。
     *
     * @return string
     */
    public function getRegisterUrl()
    {
        return $this->getPlatformUrl() . '?t=register';
    }
    
    /**
     * 清理 URL 中的 sessid 参数。
     *
     * @param string $url
     * @return string
     */
    private static function clearUrl($url)
    {
        return preg_replace("/(\?|&)sessid(=[^&]*)?&?/", '', $url);
    }


    /**
     * 使用 CURL 执行 HTTP 请求。
     *
     * @param string $uri
     * @param array $data
     * @param string $method
     * @return string
     */
    private function httpRequest($uri, $data, $method = 'POST') 
    {
        if ($method == 'GET') $uri .= '?'.http_build_query($data);
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $uri);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        if ($method == 'POST') {
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        }
        $data = curl_exec($ch);
        if ($data === false) {
            $info = curl_getinfo($ch);
            throw new Exception("CURL {$uri} 出错：" . curl_error($ch));
        } else if ($data === true){
            $info = curl_getinfo($ch);
            throw new Exception("CURL {$uri} 未返回任何数据");
        }            
        curl_close($ch);
        
        return $data;
    }
}
